-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local log = require 'mc.logging'
local vos = require 'utils.vos'
local skynet = require 'skynet'
local test_object = require 'dirty_test.optical_module_test_object'
local dirty_test_def = require 'dirty_test.dirty_test_def'
local singleton = require 'mc.singleton'
local class = require 'mc.class'

local dirty_test = class()

function dirty_test:ctor(bus)
    self.update_task_running = false    -- 测试对象信息更新协程运行标志
    self.ready_list = {}                -- 就绪列表，初始化成功则就绪，非就绪不能启动
    self.running_test_list = {}         -- 存储运行中的测试对象及配置，从外部输入获得
    self.test_objects = {}              -- 存储所有的测试对象，通过 test_object.new 创建
    self.test_object_indexes = {}       -- 对象sn到对象索引的映射
    self.bus = bus
end

-- 操作日志记录
local function log_dirty_test_operation(ctx, obj_id, dirty_cmd, cmd_result, cmd_param)
    if obj_id == nil or dirty_cmd == nil or cmd_result == nil then
        return
    end
    if dirty_cmd == dirty_test_def.cmd.INIT then
        if cmd_param == nil or cmd_param.DurationSeconds == nil then
            return
        end
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Init %s dirty test successfully, DurationSeconds(%s)', obj_id, cmd_param.DurationSeconds)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Failed to init %s dirty test, DurationSeconds(%s)', obj_id, cmd_param.DurationSeconds)
        end
    elseif dirty_cmd == dirty_test_def.cmd.START then
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Start %s dirty test successfully', obj_id)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Failed to start %s dirty test', obj_id)
        end
    elseif dirty_cmd == dirty_test_def.cmd.CLOSE then
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Shutdown %s dirty test successfully', obj_id)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Failed to shutdown %s dirty test', obj_id)
        end
    end
end

function dirty_test:find_test_object_by_npu_id(npu_id, logical_id)
    for _, test_object in pairs(self.test_objects) do
        if test_object.npu_id == npu_id and test_object.logical_id == logical_id then
            return test_object
        end
    end
    return nil
end

-- 更新对象的信息，返回更新标志
-- 注意：测试过程中出现模块不在位，直接删除对象可能导致异常。因此，模块不在位或更换时仅更新信息
function dirty_test:update_test_object(npu_id, test_object_id, test_object_presence, logical_id, op_type)
    if test_object_id == '' or test_object_id == 'N/A' then
        return
    end
    -- 1、检查是否有更换动作，即新的加入前先检查是否有老的存在并删除
    if self.test_object_indexes[test_object_id] == nil then
        local old_obj = self:find_test_object_by_npu_id(npu_id, logical_id)
        if old_obj then
            self.test_object_indexes[old_obj.test_object_id] = nil
        end 
    end
    -- 2、更新测试对象主体
    local test_obj_key = string.format('%s_%s', npu_id, logical_id)
    if self.test_objects[test_obj_key] == nil then
        log:notice('create npu id %s logical id %s dirty test object, id: %s', npu_id, logical_id, test_object_id)
        self.test_objects[test_obj_key] = test_object.new(test_object_id, test_object_presence, npu_id, self.bus, logical_id, op_type)
        -- 初始化光模块对象是否支持dirty Test
        self.test_objects[test_obj_key]:update_dirty_test_supported()
    else
        -- 光模块类型周期性通过ipmb获取，初始化测试对象的时候可能还未获取到，需要每次更新
        self.test_objects[test_obj_key].type = op_type
        if self.test_objects[test_obj_key].test_object_id ~= test_object_id then
            log:notice('update npu id %s logical id %s dirty test object id: %s', npu_id, logical_id, test_object_id)
            self.test_objects[test_obj_key].test_object_id = test_object_id
            -- 存在感知换模块但不感知不在位的情况，也需要恢复标志
            self.test_objects[test_obj_key].support_dirty_test = nil
        end
        if self.test_objects[test_obj_key].presence ~= test_object_presence then
            log:notice('update npu id %s logical id %s dirty test object presence: %s', npu_id, logical_id, test_object_presence)
            self.test_objects[test_obj_key].presence = test_object_presence
        end
    end
    -- 3、更新测试对象索引
    if test_object_presence == 0 then
        -- 不在位仅删除索引，不删除主体
        self.test_object_indexes[test_object_id] = nil
        -- 光模块不在位，是否支持dirty test属性恢复
        self.test_objects[test_obj_key].support_dirty_test = nil
    else
        if self.test_object_indexes[test_object_id] ~= test_obj_key then
            self.test_object_indexes[test_object_id] = test_obj_key
        end
    end
end

function dirty_test:get_test_object_by_object_id(object_id, assert_present)
    local test_obj_key = self.test_object_indexes[object_id]
    if test_obj_key == nil then
        return nil
    end
    if assert_present == true and self.test_objects[test_obj_key].presence == 0 then
        return nil
    end
    return self.test_objects[test_obj_key]
end

function dirty_test:update_running_test_list(test_obj)
    local running_test = {
        close_fail_count = 0,
        start_time = vos.vos_tick_get() // 1000,
        duration_seconds = test_obj.duration_second,
        test_object_id = test_obj.test_object_id
    }
    table.insert(self.running_test_list, running_test)
    log:notice('add obj %s to running test list', running_test.test_object_id)
end

function dirty_test:object_exsist_in_list(list, test_object_id)
    for _, v in pairs(list) do
        if v.test_object_id == test_object_id then
            return true
        end
    end 
    return false
end

function dirty_test:remove_from_list_by_object_id(list, test_object_id)
    for k, v in pairs(list) do
        if v.test_object_id == test_object_id then
            table.remove(list, k)
        end
    end 
end

function dirty_test:loop_init(ctx, params)
    local result = {}
    local rsp = nil
    local op_test_object = nil
    -- 再次尝试标志位
    local tried_again_flag = nil
    local detector_id
    -- 步骤1 下发dirty测试配置
    for _, v in pairs(params) do
        tried_again_flag = false
        detector_id = v.DetectorId
        op_test_object = self:get_test_object_by_object_id(detector_id, true)
        if op_test_object == nil then
            log:error('test object %s not exist', detector_id)
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Failed', v)
            result[#result + 1] = {detector_id, 'Failed'}
            goto continue
        end
        -- bmc重启场景，光模块可能处于脏污测试状态，更新一次状态
        op_test_object:get_dirty_test_result()
        -- 已经在测试中的，不可再初始化
        if op_test_object.state == dirty_test_def.dirty_state.RUNNING or self:object_exsist_in_list(self.running_test_list, v.DetectorId) then
            log:error('test object %s is running now', detector_id)
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Failed', v)
            result[#result + 1] = {detector_id, 'Failed'}
            goto continue
        end
        -- 初始化过的，不必再次初始化
        if self:object_exsist_in_list(self.ready_list, detector_id) then
            log:notice('test object %s is already init', detector_id)
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Success', v)
            result[#result + 1] = {detector_id, 'Success'}
            goto continue
        end
        ::again::
        rsp = op_test_object:init_dirty_test(v.DurationSeconds)
        if rsp == nil or rsp.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
            if not tried_again_flag then
                tried_again_flag = true
                log:error('test object %s init failed, try_again', detector_id)
                goto again
            end
            log:error('test object %s init failed', detector_id)
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Failed', v)
            result[#result + 1] = {detector_id, 'Failed'}
            op_test_object.state = dirty_test_def.dirty_state.UNINITIALIZED
        else
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Success', v)
            result[#result + 1] = {detector_id, 'Success'}
            -- 初始化成功则加入就绪列表
            self.ready_list[#self.ready_list + 1] = {test_object_id = detector_id}
            op_test_object.state = dirty_test_def.dirty_state.INITIALIZED
        end
        ::continue::
    end
    return result
end

function dirty_test:loop_start(ctx, params)
    local result = {}
    local rsp = nil
    local op_test_object = nil
    local test_object_id = nil
    -- 再次尝试标志位
    local tried_again_flag = nil
    -- 步骤1 下发dirty测试配置
    for _, v in pairs(params) do
        test_object_id = v.Detector[1].Value
        tried_again_flag = false
        op_test_object = self:get_test_object_by_object_id(test_object_id, true)
        if op_test_object == nil then
            log:error('test object %s not exist', test_object_id)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
            goto continue
        end
        -- 已经在测试中的，不可再次启动
        if self:object_exsist_in_list(self.running_test_list, test_object_id) then
            log:error('test object %s is running', test_object_id)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
            goto continue
        end
        -- 还未初始化的，不可启动
        if not self:object_exsist_in_list(self.ready_list, test_object_id) then
            log:error('test object %s is not init, can not start', test_object_id)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
            goto continue
        end
        ::again::
        rsp = op_test_object:start_dirty_test()
        if rsp == nil or rsp.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
            if not tried_again_flag then
                tried_again_flag = true
                log:error('test object %s start failed, try_again', test_object_id)
                goto again
            end
            log:error('test object %s start failed', test_object_id)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
        else
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Success', v)
            result[#result + 1] = {test_object_id, 'Success'}
            -- 配置成功的对象加入/更新到运行列表中、并且从就绪列表删除
            self:remove_from_list_by_object_id(self.ready_list, test_object_id)
            self:update_running_test_list(op_test_object)
        end
        ::continue::
    end
    return result
end

-- 轮询每个光模块的STATE\GET_RESULT
function dirty_test:running_test_update_task()
    log:notice('running test update task create')
    local test_obj = nil
    local close_ret = nil
    local current_time = nil
    while #self.running_test_list > 0 do
        -- 遍历主体对象获取信息，刷新数据
        for k, v in pairs(self.running_test_list) do
            -- 每光模块让出总线1s
            skynet.sleep(100)
            test_obj = self:get_test_object_by_object_id(v.test_object_id, false)
            if test_obj == nil then
                log:notice('obj not exist, close running test for object %s', v.test_object_id)
                self.running_test_list[k] = nil
                goto continue
            end
            -- 更新状态和统计
            test_obj:get_dirty_test_result()
            -- 若已终止测试
            if test_obj.state == dirty_test_def.dirty_state.PENDING or test_obj.state == dirty_test_def.dirty_state.ABNORMAL then
                -- 从running_test_list中移除
                log:notice('test stopped, close running test for object %s', v.test_object_id)
                self.running_test_list[k] = nil
                goto continue
            end
            -- 未超时不处理
            current_time = vos.vos_tick_get() // 1000
            if current_time - v.start_time < v.duration_seconds then
                goto continue
            end
            -- 若超时，终止测试
            close_ret = test_obj:close_dirty_test()
            if not close_ret or close_ret.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
                log:error('timeout but close running test for object %s fail', v.test_object_id)
                v.close_fail_count = v.close_fail_count + 1
            end
            -- close成功或已失败3次，不再管理
            if close_ret == dirty_test_def.rsp_code.SUCCESS or v.close_fail_count >= 3 then
                -- 恢复状态及清除本地统计记录
                test_obj.state = dirty_test_def.dirty_state.PENDING
                -- running_test_list以及ready_list中删除对象
                log:notice('timeout and close running test for object %s', v.test_object_id)
                self:remove_from_list_by_object_id(self.ready_list, v.test_object_id)
                self.running_test_list[k] = nil
            end
            ::continue::
        end
    end
    self.update_task_running = false
    self.running_test_list = {}
    log:notice('test info update task exit')
end

function dirty_test:create_running_test_update_task()
    if self.update_task_running then
        return
    end
    self.update_task_running = true
    skynet.fork_once(self.running_test_update_task, self)
end

-- dirty测试配置
-- 步骤1 下发dirty测试配置
-- 输入
-- test_objects = {
--     [1] = {
--         DetectorId = '123',
--         DurationSeconds = 300
--     },
--     ...
-- }
function dirty_test:initiate(ctx, params)
    if params == nil or next(params) == nil or #params > 8 then
        log:error('input parm is invaild')
        return {}
    end
    -- 步骤1 下发dirty测试配置
    local result = self:loop_init(ctx, params)
    if next(result) == nil then
        log:info('init_test_list is empty, do not start config task')
    end
    return result
end

-- dirty测试开始
-- 步骤1 启动dirty测试
-- 步骤2 启动协程轮询STATE\GET_RESULT
-- 输入
-- test_objects = {
--     [1] = {
--         DetectorId = '123',
--         DetectionItem = ''
--     },
--     ...
-- }
function dirty_test:start(ctx, params)
    if params == nil or next(params) == nil or #params > 8 then
        log:error('input parm is invaild')
        return {}
    end
    -- 步骤1 启动dirty测试
    local result = self:loop_start(ctx, params)
    if next(self.running_test_list) == nil then
        log:info('running_test_list is empty, do not start config task')
        return result
    end
    -- 步骤2 启动协程轮询STATE\GET_RESULT
    self:create_running_test_update_task()
    return result
end

-- 从测试主体集合中获取信息
function dirty_test:get_result(ctx, params)
    if params == nil or #params > 8 then
        log:error('input parm is invaild')
        return
    end
    log:info(params)
    local result = {}
    local op_test_object = nil
    local struct
    for _, obj_id in pairs(params) do
        struct = {Detection = {}}
        op_test_object = self:get_test_object_by_object_id(obj_id, true)
        if op_test_object == nil then
            -- 非支持对象返回unknown
            log:error('test object %s not exist', obj_id)
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectorId', Value = obj_id}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionStatus', Value = dirty_test_def.dirty_state.UNKNOWN}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionResult', Value = 'Unknown'}
            struct.Detection[#struct.Detection + 1] = {Key = 'ContaminationDistance', Value = '0'}
        else
            -- 支持对象直接获取，获取前更新一次脏污信息
            op_test_object:get_dirty_test_result()
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectorId', Value = obj_id}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionStatus', Value = op_test_object.state}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionResult', Value = op_test_object.is_dirty}
            struct.Detection[#struct.Detection + 1] = {Key = 'ContaminationDistance', Value = op_test_object.dirty_distance}
        end
        result[#result + 1] = struct
    end
    return result
end

-- 终止测试
function dirty_test:shutdown(ctx, params)
    if params == nil or #params > 8 then
        log:error('input parm is invaild')
        return {}
    end
    log:info(params)
    local op_test_object = nil
    local result = {}
    local rsp
    for _, v in pairs(params) do
        op_test_object = self:get_test_object_by_object_id(v, true)
        if op_test_object == nil then
            log:error('input test object %s not exist', v)
            log_dirty_test_operation(ctx, v, dirty_test_def.cmd.CLOSE, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
            goto continue
        end
        -- 仅下发终止测试并更新状态，running_test_list中对象配置的仍由test_info_update_task管理，防止公共变量冲突
        rsp = op_test_object:close_dirty_test()
        if not rsp or rsp.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
            log:error('shutdown test for object %s failed', v)
            log_dirty_test_operation(ctx, v, dirty_test_def.cmd.CLOSE, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
        else
            log_dirty_test_operation(ctx, v, dirty_test_def.cmd.CLOSE, 'Success', nil)
            result[#result + 1] = {v, 'Success'}
        end
        ::continue::
    end
    return result
end

return singleton(dirty_test)